/*
 * Copyright 2010, 2011, 2012 mapsforge.org
 *
 * This program is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.mapsforge.android.maps;

import org.mapsforge.android.maps.mapgenerator.MapGenerator;

import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.LinearLayout.LayoutParams;
import android.widget.ZoomControls;

/**
 * A MapZoomControls instance displays buttons for zooming in and out in a map.
 */
public class MapZoomControls
{
    private static class ZoomControlsHideHandler extends Handler
    {
        private final ZoomControls zoomControls;

        ZoomControlsHideHandler(ZoomControls zoomControls)
        {
            super();
            this.zoomControls = zoomControls;
        }

        @Override
        public void handleMessage(Message message)
        {
            this.zoomControls.hide();
        }
    }

    private static class ZoomInClickListener implements View.OnClickListener
    {
        private final MapView mapView;

        ZoomInClickListener(MapView mapView)
        {
            this.mapView = mapView;
        }

        @Override
        public void onClick(View view)
        {
            this.mapView.zoom((byte) 1, 1);
        }
    }

    private static class ZoomOutClickListener implements View.OnClickListener
    {
        private final MapView mapView;

        ZoomOutClickListener(MapView mapView)
        {
            this.mapView = mapView;
        }

        @Override
        public void onClick(View view)
        {
            this.mapView.zoom((byte) -1, 1);
        }
    }

    /**
     * Default {@link Gravity} of the zoom controls.
     */
    private static final int DEFAULT_ZOOM_CONTROLS_GRAVITY = Gravity.BOTTOM | Gravity.RIGHT;

    /**
     * Default maximum zoom level.
     */
    private static final byte DEFAULT_ZOOM_LEVEL_MAX = 22;

    /**
     * Default minimum zoom level.
     */
    private static final byte DEFAULT_ZOOM_LEVEL_MIN = 0;

    /**
     * Message code for the handler to hide the zoom controls.
     */
    private static final int MSG_ZOOM_CONTROLS_HIDE = 0;

    /**
     * Horizontal padding for the zoom controls.
     */
    private static final int ZOOM_CONTROLS_HORIZONTAL_PADDING = 5;

    /**
     * Delay in milliseconds after which the zoom controls disappear.
     */
    private static final long ZOOM_CONTROLS_TIMEOUT = ViewConfiguration.getZoomControlsTimeout();

    private boolean gravityChanged;
    private boolean showMapZoomControls;
    private final ZoomControls zoomControls;
    private int zoomControlsGravity;
    private final Handler zoomControlsHideHandler;
    private byte zoomLevelMax;
    private byte zoomLevelMin;

    MapZoomControls(Context context, final MapView mapView)
    {
        this.zoomControls = new ZoomControls(context);
        this.showMapZoomControls = true;
        this.zoomLevelMax = DEFAULT_ZOOM_LEVEL_MAX;
        this.zoomLevelMin = DEFAULT_ZOOM_LEVEL_MIN;
        this.zoomControls.setVisibility(View.GONE);
        this.zoomControlsGravity = DEFAULT_ZOOM_CONTROLS_GRAVITY;

        this.zoomControls.setOnZoomInClickListener(new ZoomInClickListener(mapView));
        this.zoomControls.setOnZoomOutClickListener(new ZoomOutClickListener(mapView));
        this.zoomControlsHideHandler = new ZoomControlsHideHandler(this.zoomControls);

        int wrapContent = android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
        LayoutParams layoutParams = new LayoutParams(wrapContent, wrapContent);
        mapView.addView(this.zoomControls, layoutParams);
    }

    /**
     * @return the current gravity for the placing of the zoom controls.
     * @see Gravity
     */
    public int getZoomControlsGravity()
    {
        return this.zoomControlsGravity;
    }

    /**
     * @return the maximum zoom level of the map.
     */
    public byte getZoomLevelMax()
    {
        return this.zoomLevelMax;
    }

    /**
     * @return the minimum zoom level of the map.
     */
    public byte getZoomLevelMin()
    {
        return this.zoomLevelMin;
    }

    /**
     * @return true if the zoom controls are visible, false otherwise.
     */
    public boolean isShowMapZoomControls()
    {
        return this.showMapZoomControls;
    }

    /**
     * @param showMapZoomControls
     *            true if the zoom controls should be visible, false otherwise.
     */
    public void setShowMapZoomControls(boolean showMapZoomControls)
    {
        this.showMapZoomControls = showMapZoomControls;
    }

    /**
     * Sets the gravity for the placing of the zoom controls. Supported values are {@link Gravity#TOP},
     * {@link Gravity#CENTER_VERTICAL}, {@link Gravity#BOTTOM}, {@link Gravity#LEFT}, {@link Gravity#CENTER_HORIZONTAL}
     * and {@link Gravity#RIGHT}.
     * 
     * @param zoomControlsGravity
     *            a combination of {@link Gravity} constants describing the desired placement.
     */
    public void setZoomControlsGravity(int zoomControlsGravity)
    {
        if (this.zoomControlsGravity != zoomControlsGravity)
        {
            this.zoomControlsGravity = zoomControlsGravity;
            this.gravityChanged = true;
        }
    }

    /**
     * Sets the maximum zoom level of the map.
     * <p>
     * The maximum possible zoom level of the MapView depends also on the current {@link MapGenerator}. For example,
     * downloading map tiles may only be possible up to a certain zoom level. Setting a higher maximum zoom level has no
     * effect in this case.
     * 
     * @param zoomLevelMax
     *            the maximum zoom level.
     * @throws IllegalArgumentException
     *             if the maximum zoom level is smaller than the current minimum zoom level.
     */
    public void setZoomLevelMax(byte zoomLevelMax)
    {
        if (zoomLevelMax < this.zoomLevelMin)
        {
            throw new IllegalArgumentException();
        }
        this.zoomLevelMax = zoomLevelMax;
    }

    /**
     * Sets the minimum zoom level of the map.
     * 
     * @param zoomLevelMin
     *            the minimum zoom level.
     * @throws IllegalArgumentException
     *             if the minimum zoom level is larger than the current maximum zoom level.
     */
    public void setZoomLevelMin(byte zoomLevelMin)
    {
        if (zoomLevelMin > this.zoomLevelMax)
        {
            throw new IllegalArgumentException();
        }
        this.zoomLevelMin = zoomLevelMin;
    }

    private int calculatePositionLeft(int left, int right, int zoomControlsWidth)
    {
        int gravity = this.zoomControlsGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
        switch (gravity)
        {
            case Gravity.LEFT:
                return ZOOM_CONTROLS_HORIZONTAL_PADDING;

            case Gravity.CENTER_HORIZONTAL:
                return (right - left - zoomControlsWidth) / 2;

            case Gravity.RIGHT:
                return right - left - zoomControlsWidth - ZOOM_CONTROLS_HORIZONTAL_PADDING;
        }

        throw new IllegalArgumentException("unknown horizontal gravity: " + gravity);
    }

    private int calculatePositionTop(int top, int bottom, int zoomControlsHeight)
    {
        int gravity = this.zoomControlsGravity & Gravity.VERTICAL_GRAVITY_MASK;
        switch (gravity)
        {
            case Gravity.TOP:
                return 0;

            case Gravity.CENTER_VERTICAL:
                return (bottom - top - zoomControlsHeight) / 2;

            case Gravity.BOTTOM:
                return bottom - top - zoomControlsHeight;
        }

        throw new IllegalArgumentException("unknown vertical gravity: " + gravity);
    }

    private void showZoomControls()
    {
        this.zoomControlsHideHandler.removeMessages(MSG_ZOOM_CONTROLS_HIDE);
        if (this.zoomControls.getVisibility() != View.VISIBLE)
        {
            this.zoomControls.show();
        }
    }

    private void showZoomControlsWithTimeout()
    {
        showZoomControls();
        this.zoomControlsHideHandler.sendEmptyMessageDelayed(MSG_ZOOM_CONTROLS_HIDE, ZOOM_CONTROLS_TIMEOUT);
    }

    int getMeasuredHeight()
    {
        return this.zoomControls.getMeasuredHeight();
    }

    int getMeasuredWidth()
    {
        return this.zoomControls.getMeasuredWidth();
    }

    void measure(int widthMeasureSpec, int heightMeasureSpec)
    {
        this.zoomControls.measure(widthMeasureSpec, heightMeasureSpec);
    }

    void onLayout(boolean changed, int left, int top, int right, int bottom)
    {
        if (!changed && !this.gravityChanged)
        {
            return;
        }

        int zoomControlsWidth = this.zoomControls.getMeasuredWidth();
        int zoomControlsHeight = this.zoomControls.getMeasuredHeight();

        int positionLeft = calculatePositionLeft(left, right, zoomControlsWidth);
        int positionTop = calculatePositionTop(top, bottom, zoomControlsHeight);
        int positionRight = positionLeft + zoomControlsWidth;
        int positionBottom = positionTop + zoomControlsHeight;

        this.zoomControls.layout(positionLeft, positionTop, positionRight, positionBottom);
        this.gravityChanged = false;
    }

    void onMapViewTouchEvent(int action)
    {
        if (this.showMapZoomControls)
        {
            switch (action)
            {
                case MotionEvent.ACTION_DOWN:
                    showZoomControls();
                    break;
                case MotionEvent.ACTION_CANCEL:
                    showZoomControlsWithTimeout();
                    break;
                case MotionEvent.ACTION_UP:
                    showZoomControlsWithTimeout();
                    break;
            }
        }
    }

    void onZoomLevelChange(int zoomLevel)
    {
        boolean zoomInEnabled = zoomLevel < this.zoomLevelMax;
        boolean zoomOutEnabled = zoomLevel > this.zoomLevelMin;

        this.zoomControls.setIsZoomInEnabled(zoomInEnabled);
        this.zoomControls.setIsZoomOutEnabled(zoomOutEnabled);
    }
}
